/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import static org.apache.solr.util.QueryMatchers.booleanQuery;
import static org.apache.solr.util.QueryMatchers.boosted;
import static org.apache.solr.util.QueryMatchers.disjunctionOf;
import static org.apache.solr.util.QueryMatchers.phraseQuery;
import static org.apache.solr.util.QueryMatchers.termQuery;
import static org.hamcrest.core.StringContains.containsString;

import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.FieldExistsQuery;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PointInSetQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.Utils;
import org.apache.solr.parser.QueryParser;
import org.apache.solr.query.FilterQuery;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.NumberType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.util.SolrMetricTestUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestSolrQueryParser extends SolrTestCaseJ4 {
  @BeforeClass
  public static void beforeClass() throws Exception {
    System.setProperty(
        "solr.index.updatelog.enabled", "false"); // schema12 doesn't support _version_
    System.setProperty("solr.max.booleanClauses", "42"); // lower for testing
    System.setProperty("solr.filterCache.async", "true"); // for testLocalParamsInQP
    initCore("solrconfig.xml", "schema12.xml");
    createIndex();
  }

  private static final List<String> HAS_VAL_FIELDS = new ArrayList<String>(41);
  private static final List<String> HAS_NAN_FIELDS = new ArrayList<String>(12);

  @AfterClass
  public static void afterClass() {
    HAS_VAL_FIELDS.clear();
    HAS_NAN_FIELDS.clear();
  }

  public static void createIndex() {
    String v;
    v = "how now brown cow";
    assertU(adoc("id", "1", "text", v, "text_np", v, "foo_i", "11"));
    v = "now cow";
    assertU(adoc("id", "2", "text", v, "text_np", v, "foo_i", "12"));
    assertU(
        adoc("id", "3", "foo_s", "a ' \" \\ {! ) } ( { z")); // A value filled with special chars

    assertU(adoc("id", "10", "qqq_s", "X"));
    assertU(adoc("id", "11", "www_s", "X"));
    assertU(adoc("id", "12", "eee_s", "X"));
    assertU(adoc("id", "13", "eee_s", "'balance'", "rrr_s", "/leading_slash"));

    assertU(adoc("id", "20", "syn", "wifi ATM"));

    { // make a doc that has a value in *lots* of fields that no other doc has
      SolrInputDocument doc = sdoc("id", "999");

      // numbers...
      for (String t : Arrays.asList("i", "l", "f", "d")) {
        for (String s : Arrays.asList("", "s", "_dv", "s_dv", "_dvo", "_norms")) {
          final String f = "has_val_" + t + s;
          HAS_VAL_FIELDS.add(f);
          doc.addField(f, "42");

          if (t.equals("f") || t.equals("d")) {
            String nanField = "nan_val_" + t + s;
            doc.addField(nanField, "NaN");
            HAS_NAN_FIELDS.add(nanField);

            // Add a NaN & non-NaN value for multivalued fields, these should match :* and :[* TO *]
            // equivalently
            if (s.startsWith("s")) {
              String bothField = "both_val_" + t + s;
              doc.addField(bothField, "42");
              doc.addField(bothField, "NaN");
              HAS_VAL_FIELDS.add(bothField);
            }
          }
        }
      }
      // boolean...booleans
      for (String s : Arrays.asList("", "s", "_dv", "_norms")) {
        final String f = "has_val_b" + s;
        HAS_VAL_FIELDS.add(f);
        doc.addField(f, "false");
      }

      // dates (and strings/text -- they don't care about the format)...
      for (String s :
          Arrays.asList("dt", "s", "s1", "t", "t_on", "dt_norms", "s_norms", "dt_dv", "s_dv")) {
        final String f = "has_val_" + s;
        HAS_VAL_FIELDS.add(f);
        doc.addField(f, "2019-01-12T00:00:00Z");
      }
      assertU(adoc(doc));
    }

    assertU(adoc("id", "30", "shingle23", "A B X D E"));

    assertU(commit());
  }

  public void testDocsWithValuesInField() throws Exception {
    assertEquals(
        "someone changed the test setup of HAS_VAL_FIELDS, w/o updating the sanity check",
        41,
        HAS_VAL_FIELDS.size());
    for (String f : HAS_VAL_FIELDS) {
      // for all of these fields, these 2 query forms should be functionally equivalent
      // in matching the one doc that contains these fields
      for (String q : Arrays.asList(f + ":*", f + ":[* TO *]")) {
        assertJQ(req("q", q), "/response/numFound==1", "/response/docs/[0]/id=='999'");
        // the same syntax should be valid even if no doc has the field...
        assertJQ(req("q", "bogus___" + q), "/response/numFound==0");
      }
    }
  }

  public void testDocsWithNaNInField() throws Exception {
    assertEquals(
        "someone changed the test setup of HAS_NAN_FIELDS, w/o updating the sanity check",
        12,
        HAS_NAN_FIELDS.size());
    for (String f : HAS_NAN_FIELDS) {
      // for all of these fields, field:* should NOT be equivalent to field:[* TO *]
      assertJQ(req("q", f + ":*"), "/response/numFound==1", "/response/docs/[0]/id=='999'");
      assertJQ(req("q", f + ":[* TO *]"), "/response/numFound==0");
      assertJQ(req("q", f + ":[-Infinity TO Infinity]"), "/response/numFound==0");
      for (String q : Arrays.asList(f + ":*", f + ":[* TO *]", f + ":[-Infinity TO Infinity]")) {
        // the same syntax should be valid even if no doc has the field...
        assertJQ(req("q", "bogus___" + q), "/response/numFound==0");
      }
    }
  }

  @Test
  public void testPhrase() {
    // "text" field's type has WordDelimiterGraphFilter (WDGFF) and autoGeneratePhraseQueries=true
    // should generate a phrase of "now cow" and match only one doc
    assertQ(req("q", "text:now-cow", "indent", "true", "sow", "true"), "//*[@numFound='1']");
    // When sow=false, autoGeneratePhraseQueries=true only works when a graph is produced
    // (i.e. overlapping terms, e.g. if WDGFF's preserveOriginal=1 or concatenateWords=1).
    // The WDGFF config on the "text" field doesn't produce a graph, so the generated query
    // is not a phrase query.  As a result, docs can match that don't match phrase query "now cow"
    assertQ(req("q", "text:now-cow", "indent", "true", "sow", "false"), "//*[@numFound='2']");
    assertQ(
        req("q", "text:now-cow", "indent", "true"), // default sow=false
        "//*[@numFound='2']");

    // "text_np" field's type has WDGFF and (default) autoGeneratePhraseQueries=false
    // should generate a query of (now OR cow) and match both docs
    assertQ(req("q", "text_np:now-cow", "indent", "true"), "//*[@numFound='2']");
  }

  @Test
  public void testLocalParamsInQP() throws Exception {
    assertJQ(req("q", "qaz {!term f=text v=$qq} wsx", "qq", "now"), "/response/numFound==2");

    assertJQ(req("q", "qaz {!term f=text v=$qq} wsx", "qq", "nomatch"), "/response/numFound==0");

    assertJQ(req("q", "qaz {!term f=text}now wsx", "qq", "now"), "/response/numFound==2");

    assertJQ(
        req("q", "qaz {!term f=foo_s v='a \\' \" \\\\ {! ) } ( { z'} wsx"), // single quote escaping
        "/response/numFound==1");

    assertJQ(
        req(
            "q",
            "qaz {!term f=foo_s v=\"a ' \\\" \\\\ {! ) } ( { z\"} wsx"), // double quote escaping
        "/response/numFound==1");

    // double-join to test back-to-back local params
    assertJQ(
        req("q", "qaz {!join from=www_s to=eee_s}{!join from=qqq_s to=www_s}id:10"),
        "/response/docs/[0]/id=='12'");
  }

  @Test
  public void testSolr4121() throws Exception {
    // At one point, balanced quotes messed up the parser(SOLR-4121)
    assertJQ(req("q", "eee_s:'balance'", "indent", "true"), "/response/numFound==1");
  }

  @Test
  public void testSyntax() throws Exception {
    // a bare * should be treated as *:*
    assertJQ(
        req("q", "*", "df", "doesnotexist_s"),
        "/response/docs/[0]==" // make sure we get something...
        );
    assertJQ(
        req("q", "doesnotexist_s:*"), "/response/numFound==0" // nothing should be found
        );
    assertJQ(
        req("q", "doesnotexist_s:( * * * )"), "/response/numFound==0" // nothing should be found
        );

    // length of date math caused issues...
    {
      SchemaField foo_dt = h.getCore().getLatestSchema().getField("foo_dt");
      String expected = "foo_dt:2013-09-11T00:00:00Z";
      if (foo_dt.getType().isPointField()) {
        expected = "foo_dt:[1378857600000 TO 1378857600000]";
        if (foo_dt.hasDocValues() && foo_dt.indexed()) {
          expected =
              "IndexOrDocValuesQuery(IndexOrDocValuesQuery(indexQuery="
                  + expected
                  + ", dvQuery="
                  + expected
                  + "))";
        } else {
          expected = "(" + expected + ")";
        }
      }
      assertJQ(
          req(
              "q",
              "foo_dt:\"2013-03-08T00:46:15Z/DAY+000MILLISECONDS+00SECONDS+00MINUTES+00HOURS+0000000000YEARS+6MONTHS+3DAYS\"",
              "debug",
              "query"),
          "/debug/parsedquery=='" + expected + "'");
    }
  }

  @Test
  public void testNestedQueryModifiers() throws Exception {
    // One previous error was that for nested queries, outer parameters overrode nested parameters.
    // For example _query_:"\"a b\"~2" was parsed as "a b"

    String subqq = "_query_:\"{!v=$qq}\"";

    assertJQ(
        req("q", "_query_:\"\\\"how brown\\\"~2\"", "debug", "query"),
        "/response/docs/[0]/id=='1'");

    assertJQ(
        req("q", subqq, "qq", "\"how brown\"~2", "debug", "query"), "/response/docs/[0]/id=='1'");

    // Should explicit slop override?  It currently does not, but that could be considered a bug.
    assertJQ(
        req("q", subqq + "~1", "qq", "\"how brown\"~2", "debug", "query"),
        "/response/docs/[0]/id=='1'");

    // Should explicit slop override?  It currently does not, but that could be considered a bug.
    assertJQ(
        req("q", "  {!v=$qq}~1", "qq", "\"how brown\"~2", "debug", "query"),
        "/response/docs/[0]/id=='1'");

    assertJQ(
        req("fq", "id:1", "fl", "id,score", "q", subqq + "^3", "qq", "text:x^2", "debug", "query"),
        "/debug/parsedquery_toString=='((text:x)^2.0)^3.0'");

    assertJQ(
        req(
            "fq",
            "id:1",
            "fl",
            "id,score",
            "q",
            "  {!v=$qq}^3",
            "qq",
            "text:x^2",
            "debug",
            "query"),
        "/debug/parsedquery_toString=='((text:x)^2.0)^3.0'");
  }

  @Test
  public void testCSQ() throws Exception {
    SolrQueryRequest req = req();

    QParser qParser = QParser.getParser("text:x^=3", req);
    Query q = qParser.getQuery();
    assertTrue(q instanceof BoostQuery);
    assertTrue(((BoostQuery) q).getQuery() instanceof ConstantScoreQuery);
    assertEquals(3.0, ((BoostQuery) q).getBoost(), 0.0f);

    req.close();
  }

  // automatically use TermsQuery when appropriate
  @Test
  public void testAutoTerms() throws Exception {
    SolrQueryRequest req = req();
    QParser qParser;
    Query q, qq;

    Map<String, String> sowFalseParamsMap = new HashMap<>();
    sowFalseParamsMap.put("sow", "false");
    Map<String, String> sowTrueParamsMap = new HashMap<>();
    sowTrueParamsMap.put("sow", "true");
    List<MapSolrParams> paramMaps =
        Arrays.asList(
            new MapSolrParams(Collections.emptyMap()), // no sow param (i.e. the default sow value)
            new MapSolrParams(sowFalseParamsMap),
            new MapSolrParams(sowTrueParamsMap));

    for (MapSolrParams params : paramMaps) {
      // relevance query should not be a filter
      qParser = QParser.getParser("foo_s:(a b c)", req);
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(3, ((BooleanQuery) q).clauses().size());

      // small filter query should still use BooleanQuery
      if (QueryParser.TERMS_QUERY_THRESHOLD > 3) {
        qParser = QParser.getParser("foo_s:(a b c)", req);
        qParser.setParams(params);
        qParser.setIsFilter(true); // this may change in the future
        q = qParser.getQuery();
        assertEquals(3, ((BooleanQuery) q).clauses().size());
      }

      // large relevancy query should use BooleanQuery
      // TODO: we may decide that string fields shouldn't have relevance in the future... change to
      // a text field w/o a stop filter if so
      qParser =
          QParser.getParser("foo_s:(a b c d e f g h i j k l m n o p q r s t u v w x y z)", req);
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(26, ((BooleanQuery) q).clauses().size());

      // large filter query should use TermsQuery
      qParser =
          QParser.getParser("foo_s:(a b c d e f g h i j k l m n o p q r s t u v w x y z)", req);
      qParser.setIsFilter(true); // this may change in the future
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(26, ((TermInSetQuery) q).getTermsCount());

      // large numeric filter query should use TermsQuery
      qParser =
          QParser.getParser("foo_ti:(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 11)", req);
      qParser.setIsFilter(true); // this may change in the future
      qParser.setParams(params);
      q = qParser.getQuery();
      if (Boolean.getBoolean(NUMERIC_POINTS_SYSPROP)) {
        if (Boolean.getBoolean(NUMERIC_DOCVALUES_SYSPROP)) {
          assertTrue(req.getCore().getLatestSchema().getField("foo_ti").hasDocValues());
          assertEquals(
              "Expecting IndexOrDocValuesQuery when type is IntPointField AND docValues are enabled",
              IndexOrDocValuesQuery.class,
              q.getClass());
          assertEquals(
              20,
              ((PointInSetQuery) ((IndexOrDocValuesQuery) q).getIndexQuery())
                  .getPackedPoints()
                  .size());
        } else {
          assertFalse(req.getCore().getLatestSchema().getField("foo_ti").hasDocValues());
          assertEquals(
              "Expecting PointInSetQuery when type is IntPointField AND docValues are disabled",
              20,
              ((PointInSetQuery) q).getPackedPoints().size());
        }
      } else {
        assertEquals(20, ((TermInSetQuery) q).getTermsCount());
      }

      // for point fields large filter query should use PointInSetQuery
      qParser =
          QParser.getParser("foo_pi:(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 11)", req);
      qParser.setIsFilter(true); // this may change in the future
      qParser.setParams(params);
      q = qParser.getQuery();

      assertTrue(req.getCore().getLatestSchema().getField("foo_pi").hasDocValues());
      assertEquals(
          "Expecting IndexOrDocValuesQuery when type is IntPointField AND docValues are enabled",
          IndexOrDocValuesQuery.class,
          q.getClass());
      assertEquals(
          20,
          ((PointInSetQuery) ((IndexOrDocValuesQuery) q).getIndexQuery()).getPackedPoints().size());

      // a filter() clause inside a relevancy query should be able to use a TermsQuery
      qParser =
          QParser.getParser(
              "foo_s:aaa filter(foo_s:(a b c d e f g h i j k l m n o p q r s t u v w x y z))", req);
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(2, ((BooleanQuery) q).clauses().size());
      qq = ((BooleanQuery) q).clauses().get(0).query();
      if (qq instanceof TermQuery) {
        qq = ((BooleanQuery) q).clauses().get(1).query();
      }

      if (qq instanceof FilterQuery) {
        qq = ((FilterQuery) qq).getQuery();
      }

      assertEquals(26, ((TermInSetQuery) qq).getTermsCount());

      // test mixed boolean query, including quotes (which shouldn't matter)
      qParser =
          QParser.getParser(
              "foo_s:(a +aaa b -bbb c d e f bar_s:(qqq www) g h i j k l m n o p q r s t u v w x y z)",
              req);
      qParser.setIsFilter(true); // this may change in the future
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(4, ((BooleanQuery) q).clauses().size());
      qq = null;
      for (BooleanClause clause : ((BooleanQuery) q).clauses()) {
        qq = clause.query();
        if (qq instanceof TermInSetQuery) break;
      }
      assertEquals(26, ((TermInSetQuery) qq).getTermsCount());

      // test terms queries of two different fields (LUCENE-7637 changed to require all terms be in
      // the same field)
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < 17; i++) {
        char letter = (char) ('a' + i);
        sb.append("foo_s:" + letter + " bar_s:" + letter + " ");
      }
      qParser = QParser.getParser(sb.toString(), req);
      qParser.setIsFilter(true); // this may change in the future
      qParser.setParams(params);
      q = qParser.getQuery();
      assertEquals(2, ((BooleanQuery) q).clauses().size());
      for (BooleanClause clause : ((BooleanQuery) q).clauses()) {
        qq = clause.query();
        assertEquals(17, ((TermInSetQuery) qq).getTermsCount());
      }
    }
    req.close();
  }

  @Test
  public void testManyClauses_Solr() throws Exception {
    final String a = "1 a 2 b 3 c 10 d 11 12 "; // 10 terms

    // this should exceed our solrconfig.xml level (solr specific) maxBooleanClauses limit
    // even though it's not long enough to trip the Lucene level (global) limit
    final String too_long = "id:(" + a + a + a + a + a + ")";

    final String expectedMsg = "Too many clauses";
    ignoreException(expectedMsg);
    SolrException e =
        expectThrows(
            SolrException.class,
            "expected SolrException",
            () -> assertJQ(req("q", too_long), "/response/numFound==6"));
    assertThat(e.getMessage(), containsString(expectedMsg));

    // but should still work as a filter query since TermsQuery can be used...
    assertJQ(req("q", "*:*", "fq", too_long), "/response/numFound==6");
    assertJQ(req("q", "*:*", "fq", too_long, "sow", "false"), "/response/numFound==6");
    assertJQ(req("q", "*:*", "fq", too_long, "sow", "true"), "/response/numFound==6");
  }

  @Test
  public void testManyClauses_Lucene() throws Exception {
    final int numZ = IndexSearcher.getMaxClauseCount();

    final String a = "1 a 2 b 3 c 10 d 11 12 "; // 10 terms
    final StringBuilder sb = new StringBuilder("id:(");
    for (int i = 0; i < numZ; i++) {
      sb.append('z').append(i).append(' ');
    }
    sb.append(a);
    sb.append(")");

    // this should trip the lucene level global BooleanQuery.getMaxClauseCount() limit,
    // causing a parsing error, before Solr even gets a chance to enforce its lower level limit
    final String way_too_long = sb.toString();

    final String expectedMsg = "too many boolean clauses";
    ignoreException(expectedMsg);
    SolrException e =
        expectThrows(
            SolrException.class,
            "expected SolrException",
            () -> assertJQ(req("q", way_too_long), "/response/numFound==6"));
    assertThat(e.getMessage(), containsString(expectedMsg));

    assertNotNull(e.getCause());
    assertEquals(SyntaxError.class, e.getCause().getClass());

    assertNotNull(e.getCause().getCause());
    assertEquals(IndexSearcher.TooManyClauses.class, e.getCause().getCause().getClass());

    // but should still work as a filter query since TermsQuery can be used...
    assertJQ(req("q", "*:*", "fq", way_too_long), "/response/numFound==6");
    assertJQ(req("q", "*:*", "fq", way_too_long, "sow", "false"), "/response/numFound==6");
    assertJQ(req("q", "*:*", "fq", way_too_long, "sow", "true"), "/response/numFound==6");
  }

  @Test
  public void testComments() throws Exception {
    assertJQ(req("q", "id:1 id:2 /* *:* */ id:3"), "/response/numFound==3");

    //
    assertJQ(
        req("q", "id:1 /**.*/"),
        "/response/numFound==1" // if it matches more than one, it's being treated as a regex.
        );

    // don't match comment start in string
    assertJQ(req("q", " \"/*\" id:1 id:2 \"*/\" id:3"), "/response/numFound==3");

    // don't match an end of comment within  a string
    // assertJQ(req("q","id:1 id:2 /* \"*/\" *:* */ id:3")
    //     ,"/response/numFound==3"
    // );
    // removed this functionality - there's more of a danger to thinking we're in a string.
    //   can't do it */  ......... '

    // nested comments
    assertJQ(
        req("q", "id:1 /* id:2 /* */ /* /**/ id:3 */ id:10 */ id:11"), "/response/numFound==2");
  }

  @Test
  public void testFilter() throws Exception {

    // normal test "solrconfig.xml" has autowarm set to 2...
    for (int i = 0; i < 10; i++) {
      assertJQ(
          req(
              "q",
              "*:* " + i,
              "fq",
              "filter(just_to_clear_the_cache) filter(id:10000"
                  + i
                  + ") filter(id:10001"
                  + i
                  + ")"),
          "/response/numFound==0");
    }
    assertU(adoc("id", "777"));
    delI("777");
    assertU(commit()); // arg... commit no longer "commits" unless there has been a change.

    CounterSnapshot.CounterDataPointSnapshot filterInsertsInitial =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsInitial =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);

    long inserts = (long) filterInsertsInitial.getValue();
    long hits = (long) filterHitsInitial.getValue();

    assertJQ(
        req("q", "doesnotexist filter(id:1) filter(qqq_s:X) filter(abcdefg)"),
        "/response/numFound==2");

    inserts += 3;
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter1 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter1 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter1.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter1.getValue());

    assertJQ(
        req("q", "doesnotexist2 filter(id:1) filter(qqq_s:X) filter(abcdefg)"),
        "/response/numFound==2");

    hits += 3;
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter2 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter2 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter2.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter2.getValue());

    // make sure normal "fq" parameters also hit the cache the same way
    assertJQ(
        req("q", "doesnotexist3", "fq", "id:1", "fq", "qqq_s:X", "fq", "abcdefg"),
        "/response/numFound==0");

    hits += 3;
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter3 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter3 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter3.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter3.getValue());

    // try a query deeply nested in a FQ
    assertJQ(
        req(
            "q",
            "*:* doesnotexist4",
            "fq",
            "(id:* +(filter(id:1) filter(qqq_s:X) filter(abcdefg)) )"),
        "/response/numFound==2");

    inserts += 1; // +1 for top level fq
    hits += 3;
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter4 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter4 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter4.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter4.getValue());

    // retry the complex FQ and make sure hashCode/equals works as expected w/ filter queries
    assertJQ(
        req(
            "q",
            "*:* doesnotexist5",
            "fq",
            "(id:* +(filter(id:1) filter(qqq_s:X) filter(abcdefg)) )"),
        "/response/numFound==2");

    hits += 1; // top-level fq should have been found.
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter5 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter5 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter5.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter5.getValue());

    // try nested filter with multiple top-level args (i.e. a boolean query)
    assertJQ(req("q", "*:* +filter(id:1 filter(qqq_s:X) abcdefg)"), "/response/numFound==2");

    hits += 1; // the inner filter
    inserts += 1; // the outer filter
    CounterSnapshot.CounterDataPointSnapshot filterInsertsAfter6 =
        SolrMetricTestUtils.getCacheSearcherOpsInserts(
            h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    CounterSnapshot.CounterDataPointSnapshot filterHitsAfter6 =
        SolrMetricTestUtils.getCacheSearcherOpsHits(h.getCore(), SolrMetricTestUtils.FILTER_CACHE);
    assertEquals("wrong number of inserts", inserts, (long) filterInsertsAfter6.getValue());
    assertEquals("wrong number of hits", hits, (long) filterHitsAfter6.getValue());

    // test the score for a filter, and that default score is 0
    assertJQ(
        req("q", "+filter(*:*) +filter(id:1)", "fl", "id,score", "sort", "id asc"),
        "/response/docs/[0]/score==0.0");

    assertJQ(
        req("q", "+filter(*:*)^=10 +filter(id:1)", "fl", "id,score", "sort", "id asc"),
        "/response/docs/[0]/score==10.0");

    assertU(adoc("id", "40", "wdf_nocase", "just some text, don't want NPE"));
    assertU(commit());

    // See SOLR-11555. If wdff removes all the characters, an NPE occurs.
    // try q and fq
    assertJQ(
        req("q", "filter(wdf_nocase:&)", "fl", "id", "debug", "query"), "/response/numFound==0");
    assertJQ(
        req("fq", "filter(wdf_nocase:.,)", "fl", "id", "debug", "query"), "/response/numFound==0");

    // Insure the same behavior as with bare clause, just not filter
    assertJQ(req("q", "wdf_nocase:&", "fl", "id", "debug", "query"), "/response/numFound==0");
    assertJQ(req("fq", "wdf_nocase:.,", "fl", "id", "debug", "query"), "/response/numFound==0");
  }

  @Test
  public void testRegex() throws Exception {
    // leading slash in a regex fixed by SOLR-8605
    assertJQ(req("q", "rrr_s:/\\/lead.*/", "fl", "id"), "/response/docs==[{id:'13'}]");
  }

  // parsing performance test
  // Run from command line with ant test -Dtestcase=TestSolrQueryParser
  // -Dtestmethod=testParsingPerformance -Dtests.asserts=false 2>/dev/null | grep QPS
  @Test
  public void testParsingPerformance() throws Exception {
    String[] args = {
      "-queries", "100", "-iter", "1000", "-clauses", "100", "-format", "term%d", "-seed", "0"
    };
    args =
        new String[] {
          "-queries", "1000", "-iter", "2000", "-clauses", "10", "-format", "term%d", "-seed", "0"
        };
    // args = new String[] {"-queries","1000" ,"-iter","1000000000", "-clauses","10",
    // "-format","term%d", "-seed","0"};

    boolean assertOn = false;
    assert assertOn = true;
    if (assertOn) {
      // System.out.println("WARNING! Assertions are enabled!!!! Will only execute small run.
      // Change with -Dtests.asserts=false");
      args =
          new String[] {
            "-queries", "10", "-iter", "2", "-clauses", "20", "-format", "term%d", "-seed", "0"
          };
    }

    int iter = 1000;
    int numQueries = 100;
    int maxClauses = 5;
    int maxTerm = 10000000;
    String format = "term%d";
    String field = "foo_s";
    long seed = 0;
    boolean isFilter = true;
    boolean rewrite = false;

    String otherStuff = "";

    for (int i = 0; i < args.length; i++) {
      String a = args[i];
      if ("-queries".equals(a)) {
        numQueries = Integer.parseInt(args[++i]);
      } else if ("-iter".equals(a)) {
        iter = Integer.parseInt(args[++i]);
      } else if ("-clauses".equals(a)) {
        maxClauses = Integer.parseInt(args[++i]);
      } else if ("-format".equals(a)) {
        format = args[++i];
      } else if ("-seed".equals(a)) {
        seed = Long.parseLong(args[++i]);
      } else {
        otherStuff = otherStuff + " " + a;
      }
    }

    Random r = new Random(seed);

    String[] queries = new String[numQueries];
    for (int i = 0; i < queries.length; i++) {
      StringBuilder sb = new StringBuilder();
      boolean explicitField = r.nextInt(5) == 0;
      if (!explicitField) {
        sb.append(field + ":(");
      }

      sb.append(otherStuff).append(" ");

      int nClauses =
          r.nextInt(maxClauses) + 1; // TODO: query parse can't parse () for some reason???

      for (int c = 0; c < nClauses; c++) {
        String termString = String.format(Locale.US, format, r.nextInt(maxTerm));
        if (explicitField) {
          sb.append(field).append(':');
        }
        sb.append(termString);
        sb.append(' ');
      }

      if (!explicitField) {
        sb.append(")");
      }
      queries[i] = sb.toString();
      // System.out.println(queries[i]);
    }

    SolrQueryRequest req = req();

    long start = System.nanoTime();

    int ret = 0;
    for (int i = 0; i < iter; i++) {
      for (String qStr : queries) {
        QParser parser = QParser.getParser(qStr, req);
        parser.setIsFilter(isFilter);
        Query q = parser.getQuery();
        if (rewrite) {
          // TODO: do rewrite
        }
        ret += q.getClass().hashCode(); // use the query somehow
      }
    }

    long end = System.nanoTime();

    System.out.println(
        (assertOn ? "WARNING, assertions enabled. " : "")
            + "ret="
            + ret
            + " Parser QPS:"
            + ((long) numQueries * iter) * 1000000000 / (end - start));

    req.close();
  }

  @Test
  public void testSplitOnWhitespace_Basic() throws Exception {
    // The "syn" field has synonyms loaded from synonyms.txt

    assertJQ(
        req(
            "df", "syn", "q", "wifi", "sow",
            "true"), // retrieve the single document containing literal "wifi"
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(
        req("df", "syn", "q", "wi fi", "sow", "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(req("df", "syn", "q", "wi fi", "sow", "true"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}wi fi"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(req("df", "syn", "q", "{!lucene sow=true}wi fi"), "/response/numFound==0");

    assertJQ(
        req("df", "syn", "q", "{!lucene}wi fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
  }

  public void testSplitOnWhitespace_Comments() throws Exception {
    // The "syn" field has synonyms loaded from synonyms.txt

    assertJQ(
        req(
            "df", "syn", "q", "wifi", "sow",
            "true"), // retrieve the single document containing literal "wifi"
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "wi fi", "sow", "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req(
            "df",
            "syn",
            "q",
            "wi /* foo */ fi",
            "sow",
            "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req(
            "df",
            "syn",
            "q",
            "wi /* foo */ /* bar */ fi",
            "sow",
            "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req(
            "df",
            "syn",
            "q",
            " /* foo */ wi fi /* bar */",
            "sow",
            "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req(
            "df",
            "syn",
            "q",
            " /* foo */ wi /* bar */ fi /* baz */",
            "sow",
            "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(req("df", "syn", "q", "wi fi", "sow", "true"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi /* foo */ fi", "sow", "true"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi /* foo */ /* bar */ fi", "sow", "true"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "/* foo */ wi fi /* bar */", "sow", "true"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "/* foo */ wi /* bar */ fi /* baz */", "sow", "true"),
        "/response/numFound==0");

    assertJQ(
        req("df", "syn", "q", "wi fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "wi /* foo */ fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "wi /* foo */ /* bar */ fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", " /* foo */ wi fi /* bar */"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", " /* foo */ wi /* bar */ fi /* baz */"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}wi fi"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}wi /* foo */ fi"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}wi /* foo */ /* bar */ fi"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}/* foo */ wi fi /* bar */"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=false}/* foo */ wi /* bar */ fi /* baz */"),
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(req("df", "syn", "q", "{!lucene sow=true}wi fi"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "{!lucene sow=true}wi /* foo */ fi"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=true}wi /* foo */ /* bar */ fi"),
        "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=true}/* foo */ wi fi /* bar */"),
        "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "{!lucene sow=true}/* foo */ wi /* bar */ fi /* baz */"),
        "/response/numFound==0");

    assertJQ(
        req("df", "syn", "q", "{!lucene}wi fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene}wi /* foo */ fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene}wi /* foo */ /* bar */ fi"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene}/* foo */ wi fi /* bar */"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "{!lucene}/* foo */ wi /* bar */ fi /* baz */"), // default sow=false
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
  }

  public void testOperatorsAndMultiWordSynonyms() throws Exception {
    // The "syn" field has synonyms loaded from synonyms.txt

    assertJQ(
        req(
            "df", "syn", "q", "wifi", "sow",
            "true"), // retrieve the single document containing literal "wifi"
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");
    assertJQ(
        req("df", "syn", "q", "wi fi", "sow", "false"), // trigger the "wi fi => wifi" synonym
        "/response/numFound==1",
        "/response/docs/[0]/id=='20'");

    assertJQ(req("df", "syn", "q", "+wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "-wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "!wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi* fi", "sow", "false"), // matches because wi* matches wifi
        "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "w? fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi~1 fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi^2 fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi^=2 fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi +fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi -fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi !fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi*", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi?", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi~1", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi^2", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi^=2", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "syn:wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi syn:fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "NOT wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi NOT fi", "sow", "false"), "/response/numFound==0");

    assertJQ(req("df", "syn", "q", "wi fi AND ATM", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "ATM AND wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi fi && ATM", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "ATM && wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "(wi fi) AND ATM", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "ATM AND (wi fi)", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "(wi fi) && ATM", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "ATM && (wi fi)", "sow", "false"), "/response/numFound==1");

    assertJQ(
        req("df", "syn", "q", "wi fi OR NotThereAtAll", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "NotThereAtAll OR wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi fi || NotThereAtAll", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "NotThereAtAll || wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "(wi fi) OR NotThereAtAll", "sow", "false"), "/response/numFound==1");
    assertJQ(
        req("df", "syn", "q", "NotThereAtAll OR (wi fi)", "sow", "false"), "/response/numFound==1");
    assertJQ(
        req("df", "syn", "q", "(wi fi) || NotThereAtAll", "sow", "false"), "/response/numFound==1");
    assertJQ(
        req("df", "syn", "q", "NotThereAtAll || (wi fi)", "sow", "false"), "/response/numFound==1");

    assertJQ(req("df", "syn", "q", "\"wi\" fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi \"fi\"", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "(wi) fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi (fi)", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "/wi/ fi", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "wi /fi/", "sow", "false"), "/response/numFound==0");
    assertJQ(req("df", "syn", "q", "(wi fi)", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "+(wi fi)", "sow", "false"), "/response/numFound==1");

    @SuppressWarnings({"rawtypes"})
    Map all = (Map) Utils.fromJSONString(h.query(req("q", "*:*", "rows", "0", "wt", "json")));
    int totalDocs = Integer.parseInt(((Map) all.get("response")).get("numFound").toString());
    int allDocsExceptOne = totalDocs - 1;

    assertJQ(
        req("df", "syn", "q", "-(wi fi)", "sow", "false"),
        "/response/numFound==" + allDocsExceptOne // one doc contains "wifi" in the syn field
        );
    assertJQ(
        req("df", "syn", "q", "!(wi fi)", "sow", "false"),
        "/response/numFound==" + allDocsExceptOne // one doc contains "wifi" in the syn field
        );
    assertJQ(
        req("df", "syn", "q", "NOT (wi fi)", "sow", "false"),
        "/response/numFound==" + allDocsExceptOne // one doc contains "wifi" in the syn field
        );
    assertJQ(req("df", "syn", "q", "(wi fi)^2", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "(wi fi)^=2", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "syn:(wi fi)", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "+ATM wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "-ATM wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "-NotThereAtAll wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "!ATM wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "!NotThereAtAll wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "NOT ATM wi fi", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "NOT NotThereAtAll wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "AT* wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "AT? wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "\"ATM\" wi fi", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi +ATM", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi -ATM", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi fi -NotThereAtAll", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi !ATM", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi fi !NotThereAtAll", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi NOT ATM", "sow", "false"), "/response/numFound==0");
    assertJQ(
        req("df", "syn", "q", "wi fi NOT NotThereAtAll", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi AT*", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi AT?", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "wi fi \"ATM\"", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "\"wi fi\"~2", "sow", "false"), "/response/numFound==1");
    assertJQ(req("df", "syn", "q", "syn:\"wi fi\"", "sow", "false"), "/response/numFound==1");
  }

  @Test
  public void testAutoGeneratePhraseQueries() throws Exception {
    ModifiableSolrParams noSowParams = new ModifiableSolrParams();
    ModifiableSolrParams sowFalseParams = new ModifiableSolrParams();
    sowFalseParams.add("sow", "false");
    ModifiableSolrParams sowTrueParams = new ModifiableSolrParams();
    sowTrueParams.add("sow", "true");

    // From synonyms.txt:
    //
    //     crow blackbird, grackle
    //
    try (SolrQueryRequest req = req()) {

      for (SolrParams params : Arrays.asList(noSowParams, sowFalseParams)) {
        QParser qParser =
            QParser.getParser("text:grackle", req); // "text" has autoGeneratePhraseQueries="true"
        qParser.setParams(sowFalseParams);
        Query q = qParser.getQuery();
        assertEquals("(text:\"crow blackbird\" text:grackl)", q.toString());
      }

      QParser qParser = QParser.getParser("text:grackle", req);
      qParser.setParams(sowTrueParams);
      Query q = qParser.getQuery();
      assertEquals("text:\"crow blackbird\" text:grackl", q.toString());

      for (SolrParams params : Arrays.asList(noSowParams, sowTrueParams, sowFalseParams)) {
        qParser =
            QParser.getParser(
                "text_sw:grackle",
                req); // "text_sw" doesn't specify autoGeneratePhraseQueries => default false
        qParser.setParams(params);
        q = qParser.getQuery();
        assertEquals("((+text_sw:crow +text_sw:blackbird) text_sw:grackl)", q.toString());
      }
    }
  }

  @Test
  public void testShingleQueries() throws Exception {
    ModifiableSolrParams sowFalseParams = new ModifiableSolrParams();
    sowFalseParams.add("sow", "false");

    try (SolrQueryRequest req = req(sowFalseParams)) {
      QParser qParser = QParser.getParser("shingle23:(A B C)", req);
      Query q = qParser.getQuery();
      assertEquals("Synonym(shingle23:A_B shingle23:A_B_C) shingle23:B_C", q.toString());
    }

    assertJQ(req("df", "shingle23", "q", "A B C", "sow", "false"), "/response/numFound==1");
  }

  public void testSynonymQueryStyle() throws Exception {
    String field = "t_pick_best_foo";
    Query q = QParser.getParser("tabby", req(params("df", field))).getQuery();
    assertThat(
        q,
        disjunctionOf(
            termQuery(field, "cat"),
            termQuery(field, "tabbi"),
            termQuery(field, "anim"),
            termQuery(field, "felin")));

    field = "t_as_distinct_foo";
    q = QParser.getParser("tabby", req(params("df", field))).getQuery();
    assertThat(
        q,
        booleanQuery(
            termQuery(field, "cat"),
            termQuery(field, "tabbi"),
            termQuery(field, "anim"),
            termQuery(field, "felin")));

    /*confirm autoGeneratePhraseQueries always builds OR queries*/
    q =
        QParser.getParser("jeans", req(params("df", "t_as_distinct_foo", "sow", "false")))
            .getQuery();
    assertEquals("(t_as_distinct_foo:\"denim pant\" t_as_distinct_foo:jean)", q.toString());

    field = "t_pick_best_foo";
    q = QParser.getParser("jeans", req(params("df", field, "sow", "false"))).getQuery();
    assertThat(
        q, booleanQuery(disjunctionOf(termQuery(field, "jean"), phraseQuery(field, "denim pant"))));
  }

  public void testSynonymsBoost_singleTermQuerySingleTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // tiger, tigre|0.9
    String field = "t_pick_best_boosted_foo";
    Query q = QParser.getParser("tiger", req(params("df", field))).getQuery();
    assertThat(q, disjunctionOf(termQuery(field, "tiger"), boosted(field, "tigre", 0.9f)));

    field = "t_as_distinct_boosted_foo";
    q = QParser.getParser("tiger", req(params("df", "t_as_distinct_boosted_foo"))).getQuery();
    assertEquals(
        "(t_as_distinct_boosted_foo:tigre)^0.9 t_as_distinct_boosted_foo:tiger", q.toString());

    field = "t_as_same_term_boosted_foo";
    q = QParser.getParser("tiger", req(params("df", "t_as_same_term_boosted_foo"))).getQuery();
    assertEquals(
        "Synonym(t_as_same_term_boosted_foo:tiger t_as_same_term_boosted_foo:tigre^0.9)",
        q.toString());

    // lynx => lince|0.8, lynx_canadensis|0.9
    field = "t_pick_best_boosted_foo";
    q = QParser.getParser("lynx", req(params("df", field))).getQuery();
    assertThat(
        q, disjunctionOf(boosted(field, "lince", 0.8f), boosted(field, "lynx_canadensis", 0.9f)));

    field = "t_as_distinct_boosted_foo";
    q = QParser.getParser("lynx", req(params("df", field))).getQuery();
    assertThat(
        q, booleanQuery(boosted(field, "lince", 0.8f), boosted(field, "lynx_canadensis", 0.9f)));

    field = "t_as_same_term_boosted_foo";
    q = QParser.getParser("lynx", req(params("df", field))).getQuery();
    assertEquals(
        "Synonym(t_as_same_term_boosted_foo:lince^0.8 t_as_same_term_boosted_foo:lynx_canadensis^0.9)",
        q.toString());
  }

  public void testSynonymsBoost_singleTermQueryMultiTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // leopard, big cat|0.8, bagheera|0.9, panthera pardus|0.85
    String field = "t_pick_best_boosted_foo";
    Query q = QParser.getParser("leopard", req(params("df", "t_pick_best_boosted_foo"))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                termQuery(field, "leopard"),
                boosted(phraseQuery(field, "big cat"), 0.8f),
                boosted(phraseQuery(field, "panthera pardus"), 0.85f),
                boosted(termQuery(field, "bagheera"), 0.9f))));

    q = QParser.getParser("leopard", req(params("df", "t_as_distinct_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:bagheera)^0.9 (t_as_distinct_boosted_foo:\"panthera pardus\")^0.85 t_as_distinct_boosted_foo:leopard)",
        q.toString());

    q = QParser.getParser("leopard", req(params("df", "t_as_same_term_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"big cat\")^0.8 (t_as_same_term_boosted_foo:bagheera)^0.9 (t_as_same_term_boosted_foo:\"panthera pardus\")^0.85 t_as_same_term_boosted_foo:leopard)",
        q.toString());

    // lion => panthera leo|0.9, simba leo|0.8, kimba|0.75
    q = QParser.getParser("lion", req(params("df", "t_pick_best_boosted_foo"))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                boosted(termQuery(field, "kimba"), 0.75f),
                boosted(phraseQuery(field, "simba leo"), 0.8f),
                boosted(phraseQuery(field, "panthera leo"), 0.9f))));

    q = QParser.getParser("lion", req(params("df", "t_as_distinct_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"panthera leo\")^0.9 (t_as_distinct_boosted_foo:\"simba leo\")^0.8 (t_as_distinct_boosted_foo:kimba)^0.75)",
        q.toString());

    q = QParser.getParser("lion", req(params("df", "t_as_same_term_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"panthera leo\")^0.9 (t_as_same_term_boosted_foo:\"simba leo\")^0.8 (t_as_same_term_boosted_foo:kimba)^0.75)",
        q.toString());
  }

  public void testSynonymsBoost_multiTermQuerySingleTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // tiger, tigre|0.9
    // lynx => lince|0.8, lynx_canadensis|0.9
    String field = "t_pick_best_boosted_foo";
    Query q = QParser.getParser("tiger lynx", req(params("df", field))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(boosted(field, "lince", 0.8f), boosted(field, "lynx_canadensis", 0.9f)),
            disjunctionOf(termQuery(field, "tiger"), boosted(field, "tigre", 0.9f))));

    q = QParser.getParser("tiger lynx", req(params("df", "t_as_distinct_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:tigre)^0.9 t_as_distinct_boosted_foo:tiger)"
            + " ((t_as_distinct_boosted_foo:lince)^0.8 (t_as_distinct_boosted_foo:lynx_canadensis)^0.9)",
        q.toString());

    q = QParser.getParser("tiger lynx", req(params("df", "t_as_same_term_boosted_foo"))).getQuery();
    assertEquals(
        "Synonym(t_as_same_term_boosted_foo:tiger t_as_same_term_boosted_foo:tigre^0.9)"
            + " Synonym(t_as_same_term_boosted_foo:lince^0.8 t_as_same_term_boosted_foo:lynx_canadensis^0.9)",
        q.toString());
  }

  public void testSynonymsBoost_multiTermQueryMultiTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // leopard, big cat|0.8, bagheera|0.9, panthera pardus|0.85
    // lion => panthera leo|0.9, simba leo|0.8, kimba|0.75
    String field = "t_pick_best_boosted_foo";
    Query q = QParser.getParser("leopard lion", req(params("df", field))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                termQuery(field, "leopard"),
                boosted(phraseQuery(field, "big cat"), 0.8f),
                boosted(phraseQuery(field, "panthera pardus"), 0.85f),
                boosted(termQuery(field, "bagheera"), 0.9f)),
            disjunctionOf(
                boosted(termQuery(field, "kimba"), 0.75f),
                boosted(phraseQuery(field, "simba leo"), 0.8f),
                boosted(phraseQuery(field, "panthera leo"), 0.9f))));

    q =
        QParser.getParser("leopard lion", req(params("df", "t_as_distinct_boosted_foo")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:bagheera)^0.9 (t_as_distinct_boosted_foo:\"panthera pardus\")^0.85 t_as_distinct_boosted_foo:leopard)"
            + " ((t_as_distinct_boosted_foo:\"panthera leo\")^0.9 (t_as_distinct_boosted_foo:\"simba leo\")^0.8 (t_as_distinct_boosted_foo:kimba)^0.75)",
        q.toString());

    q =
        QParser.getParser("leopard lion", req(params("df", "t_as_same_term_boosted_foo")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"big cat\")^0.8 (t_as_same_term_boosted_foo:bagheera)^0.9 (t_as_same_term_boosted_foo:\"panthera pardus\")^0.85 t_as_same_term_boosted_foo:leopard)"
            + " ((t_as_same_term_boosted_foo:\"panthera leo\")^0.9 (t_as_same_term_boosted_foo:\"simba leo\")^0.8 (t_as_same_term_boosted_foo:kimba)^0.75)",
        q.toString());
  }

  public void testSynonymsBoost_singleConceptQuerySingleTermSynonym_shouldParseBoostedQuery()
      throws Exception {
    // panthera pardus, leopard|0.6
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser("panthera pardus story", req(params("df", field, "sow", "false")))
            .getQuery();
    assertThat(
        q,
        booleanQuery(
            termQuery(field, "story"),
            disjunctionOf(boosted(field, "leopard", 0.6f), phraseQuery(field, "panthera pardus"))));

    q =
        QParser.getParser(
                "panthera pardus story",
                req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:leopard)^0.6 t_as_distinct_boosted_foo:\"panthera pardus\") t_as_distinct_boosted_foo:story",
        q.toString());

    q =
        QParser.getParser(
                "panthera pardus story",
                req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:leopard)^0.6 t_as_same_term_boosted_foo:\"panthera pardus\") t_as_same_term_boosted_foo:story",
        q.toString());

    // panthera tigris => tiger|0.99
    q =
        QParser.getParser(
                "panthera tigris story",
                req(params("df", "t_pick_best_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "(t_pick_best_boosted_foo:tiger)^0.99 t_pick_best_boosted_foo:story", q.toString());

    q =
        QParser.getParser(
                "panthera tigris story",
                req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "(t_as_distinct_boosted_foo:tiger)^0.99 t_as_distinct_boosted_foo:story", q.toString());

    q =
        QParser.getParser(
                "panthera tigris story",
                req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "(t_as_same_term_boosted_foo:tiger)^0.99 t_as_same_term_boosted_foo:story", q.toString());
  }

  public void
      testSynonymsBoost_singleConceptQueryMultiTermSynonymWithMultipleBoost_shouldParseMultiplicativeBoostedQuery()
          throws Exception {
    // panthera blytheae, oldest|0.5 ancient|0.9 panthera
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser("panthera blytheae", req(params("df", field, "sow", "false"))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                boosted(phraseQuery(field, "oldest ancient panthera"), 0.45f),
                phraseQuery(field, "panthera blytheae"))));

    q =
        QParser.getParser(
                "panthera blytheae", req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"oldest ancient panthera\")^0.45 t_as_distinct_boosted_foo:\"panthera blytheae\")",
        q.toString());

    q =
        QParser.getParser(
                "panthera blytheae",
                req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"oldest ancient panthera\")^0.45 t_as_same_term_boosted_foo:\"panthera blytheae\")",
        q.toString());
  }

  public void testSynonymsBoost_singleConceptQueryMultiTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // snow leopard, panthera uncia|0.9, big cat|0.8, white_leopard|0.6
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser("snow leopard", req(params("df", field, "sow", "false"))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf( // TODO why does this generate a single clause Boolean?
                phraseQuery(field, "snow leopard"),
                boosted(phraseQuery(field, "panthera uncia"), 0.9f),
                boosted(phraseQuery(field, "big cat"), 0.8f),
                boosted(field, "white_leopard", 0.6f))));

    q =
        QParser.getParser(
                "snow leopard", req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"panthera uncia\")^0.9 (t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:white_leopard)^0.6 t_as_distinct_boosted_foo:\"snow leopard\")",
        q.toString());

    q =
        QParser.getParser(
                "snow leopard", req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"panthera uncia\")^0.9 (t_as_same_term_boosted_foo:\"big cat\")^0.8 (t_as_same_term_boosted_foo:white_leopard)^0.6 t_as_same_term_boosted_foo:\"snow leopard\")",
        q.toString());

    // panthera onca => jaguar|0.95, big cat|0.85, black panther|0.65
    q =
        QParser.getParser(
                "panthera onca", req(params("df", "t_pick_best_boosted_foo", "sow", "false")))
            .getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                boosted(field, "jaguar", 0.95f),
                boosted(phraseQuery(field, "big cat"), 0.85f),
                boosted(phraseQuery(field, "black panther"), 0.65f))));

    q =
        QParser.getParser(
                "panthera onca", req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:jaguar)^0.95 (t_as_distinct_boosted_foo:\"big cat\")^0.85 (t_as_distinct_boosted_foo:\"black panther\")^0.65)",
        q.toString());

    q =
        QParser.getParser(
                "panthera onca", req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:jaguar)^0.95 (t_as_same_term_boosted_foo:\"big cat\")^0.85 (t_as_same_term_boosted_foo:\"black panther\")^0.65)",
        q.toString());
  }

  public void testSynonymsBoost_multiConceptQuerySingleTermSynonym_shouldParseBoostedQuery()
      throws Exception {
    // panthera pardus, leopard|0.6
    // tiger, tigre|0.9
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser("panthera pardus tiger", req(params("df", field, "sow", "false")))
            .getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(boosted(field, "leopard", 0.6f), phraseQuery(field, "panthera pardus")),
            disjunctionOf(termQuery(field, "tiger"), boosted(field, "tigre", 0.9f))));

    q =
        QParser.getParser(
                "panthera pardus tiger",
                req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:leopard)^0.6 t_as_distinct_boosted_foo:\"panthera pardus\") ((t_as_distinct_boosted_foo:tigre)^0.9 t_as_distinct_boosted_foo:tiger)",
        q.toString());

    q =
        QParser.getParser(
                "panthera pardus tiger",
                req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:leopard)^0.6 t_as_same_term_boosted_foo:\"panthera pardus\") Synonym(t_as_same_term_boosted_foo:tiger t_as_same_term_boosted_foo:tigre^0.9)",
        q.toString());
  }

  public void testSynonymsBoost_multiConceptsQueryMultiTermSynonyms_shouldParseBoostedQuery()
      throws Exception {
    // snow leopard, panthera uncia|0.9, big cat|0.8, white_leopard|0.6
    // panthera onca => jaguar|0.95, big cat|0.85, black panther|0.65
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser("snow leopard panthera onca", req(params("df", field, "sow", "false")))
            .getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                boosted(phraseQuery(field, "panthera uncia"), 0.9f),
                boosted(phraseQuery(field, "big cat"), 0.8f),
                boosted(field, "white_leopard", 0.6f),
                phraseQuery(field, "snow leopard")),
            disjunctionOf(
                boosted(field, "jaguar", 0.95f),
                boosted(phraseQuery(field, "big cat"), 0.85f),
                boosted(phraseQuery(field, "black panther"), 0.65f))));

    q =
        QParser.getParser(
                "snow leopard panthera onca",
                req(params("df", "t_as_distinct_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"panthera uncia\")^0.9 (t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:white_leopard)^0.6 t_as_distinct_boosted_foo:\"snow leopard\")"
            + " ((t_as_distinct_boosted_foo:jaguar)^0.95 (t_as_distinct_boosted_foo:\"big cat\")^0.85 (t_as_distinct_boosted_foo:\"black panther\")^0.65)",
        q.toString());

    q =
        QParser.getParser(
                "snow leopard panthera onca",
                req(params("df", "t_as_same_term_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "((t_as_same_term_boosted_foo:\"panthera uncia\")^0.9 (t_as_same_term_boosted_foo:\"big cat\")^0.8 (t_as_same_term_boosted_foo:white_leopard)^0.6 t_as_same_term_boosted_foo:\"snow leopard\")"
            + " ((t_as_same_term_boosted_foo:jaguar)^0.95 (t_as_same_term_boosted_foo:\"big cat\")^0.85 (t_as_same_term_boosted_foo:\"black panther\")^0.65)",
        q.toString());
  }

  public void testSynonymsBoost_edismaxBoost_shouldParseBoostedPhraseQuery() throws Exception {
    String field = "t_pick_best_boosted_foo";
    Query q =
        QParser.getParser(
                "snow leopard lion",
                "edismax",
                true,
                req(params("sow", "false", "qf", field + "^10")))
            .getQuery();
    assertThat(
        q,
        booleanQuery(
            booleanQuery(
                disjunctionOf(
                    boosted(
                        disjunctionOf(
                            phraseQuery(field, "snow leopard"),
                            boosted(phraseQuery(field, "big cat"), 0.8f),
                            boosted(phraseQuery(field, "panthera uncia"), 0.9f),
                            boosted(termQuery(field, "white_leopard"), 0.6f)),
                        10)),
                disjunctionOf(
                    boosted(
                        disjunctionOf(
                            boosted(termQuery(field, "kimba"), 0.75f),
                            boosted(phraseQuery(field, "simba leo"), 0.8f),
                            boosted(phraseQuery(field, "panthera leo"), 0.9f)),
                        10))),
            BooleanClause.Occur.MUST));

    q =
        QParser.getParser(
                "snow leopard lion",
                "edismax",
                true,
                req(params("sow", "false", "qf", "t_as_distinct_boosted_foo^10")))
            .getQuery();
    assertEquals(
        "+("
            + "(((t_as_distinct_boosted_foo:\"panthera uncia\")^0.9 (t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:white_leopard)^0.6 t_as_distinct_boosted_foo:\"snow leopard\")^10.0)"
            + " (((t_as_distinct_boosted_foo:\"panthera leo\")^0.9 (t_as_distinct_boosted_foo:\"simba leo\")^0.8 (t_as_distinct_boosted_foo:kimba)^0.75)^10.0))",
        q.toString());

    q =
        QParser.getParser(
                "snow leopard lion",
                "edismax",
                true,
                req(params("sow", "false", "qf", "t_as_same_term_boosted_foo^10")))
            .getQuery();
    assertEquals(
        "+("
            + "(((t_as_same_term_boosted_foo:\"panthera uncia\")^0.9 (t_as_same_term_boosted_foo:\"big cat\")^0.8 (t_as_same_term_boosted_foo:white_leopard)^0.6 t_as_same_term_boosted_foo:\"snow leopard\")^10.0)"
            + " (((t_as_same_term_boosted_foo:\"panthera leo\")^0.9 (t_as_same_term_boosted_foo:\"simba leo\")^0.8 (t_as_same_term_boosted_foo:kimba)^0.75)^10.0))",
        q.toString());
  }

  public void testSynonymsBoost_phraseQueryMultiTermSynonymsBoost() throws Exception {
    Query q =
        QParser.getParser(
                "\"snow leopard lion\"",
                req(params("df", "t_pick_best_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "(t_pick_best_boosted_foo:\"panthera uncia panthera leo\")^0.80999994 "
            + "(t_pick_best_boosted_foo:\"panthera uncia simba leo\")^0.71999997 "
            + "(t_pick_best_boosted_foo:\"panthera uncia kimba\")^0.67499995 "
            + "(t_pick_best_boosted_foo:\"big cat panthera leo\")^0.71999997 "
            + "(t_pick_best_boosted_foo:\"big cat simba leo\")^0.64000005 "
            + "(t_pick_best_boosted_foo:\"big cat kimba\")^0.6 "
            + "(t_pick_best_boosted_foo:\"white_leopard panthera leo\")^0.54 "
            + "(t_pick_best_boosted_foo:\"white_leopard simba leo\")^0.48000002 "
            + "(t_pick_best_boosted_foo:\"white_leopard kimba\")^0.45000002 "
            + "(t_pick_best_boosted_foo:\"snow leopard panthera leo\")^0.9 "
            + "(t_pick_best_boosted_foo:\"snow leopard simba leo\")^0.8 "
            + "(t_pick_best_boosted_foo:\"snow leopard kimba\")^0.75",
        q.toString());
  }

  public void testSynonymsBoost_phraseQueryMultiTermSynonymsMultipleBoost() throws Exception {
    Query q =
        QParser.getParser(
                "\"panthera blytheae lion\"",
                req(params("df", "t_pick_best_boosted_foo", "sow", "false")))
            .getQuery();
    assertEquals(
        "(t_pick_best_boosted_foo:\"oldest ancient panthera panthera leo\")^0.40499997 "
            + "(t_pick_best_boosted_foo:\"oldest ancient panthera simba leo\")^0.35999998 "
            + "(t_pick_best_boosted_foo:\"oldest ancient panthera kimba\")^0.33749998 "
            + "(t_pick_best_boosted_foo:\"panthera blytheae panthera leo\")^0.9 "
            + "(t_pick_best_boosted_foo:\"panthera blytheae simba leo\")^0.8 "
            + "(t_pick_best_boosted_foo:\"panthera blytheae kimba\")^0.75",
        q.toString());
  }

  public void testSynonymsBoost_BoostMissing_shouldAssignDefaultBoost() throws Exception {
    // leopard, big cat|0.8, bagheera|0.9, panthera pardus|0.85
    String field = "t_pick_best_boosted_foo";
    Query q = QParser.getParser("leopard", req(params("df", field))).getQuery();
    assertThat(
        q,
        booleanQuery(
            disjunctionOf(
                termQuery(field, "leopard"),
                boosted(phraseQuery(field, "big cat"), 0.8f),
                boosted(phraseQuery(field, "panthera pardus"), 0.85f),
                boosted(termQuery(field, "bagheera"), 0.9f))));

    q = QParser.getParser("leopard", req(params("df", "t_as_distinct_boosted_foo"))).getQuery();
    assertEquals(
        "((t_as_distinct_boosted_foo:\"big cat\")^0.8 (t_as_distinct_boosted_foo:bagheera)^0.9 (t_as_distinct_boosted_foo:\"panthera pardus\")^0.85 t_as_distinct_boosted_foo:leopard)",
        q.toString());
  }

  @Test
  public void testBadRequestInSetQuery() throws SyntaxError {
    SolrQueryRequest req = req();
    QParser qParser;
    String[] fieldSuffix =
        new String[] {
          "ti", "tf", "td", "tl",
          "i", "f", "d", "l",
          "is", "fs", "ds", "ls",
          "i_dv", "f_dv", "d_dv", "l_dv",
          "is_dv", "fs_dv", "ds_dv", "ls_dv",
          "i_dvo", "f_dvo", "d_dvo", "l_dvo",
        };

    for (String suffix : fieldSuffix) {
      // Good queries
      qParser =
          QParser.getParser(
              "foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 25)", req);
      qParser.setIsFilter(true);
      qParser.getQuery();
    }

    for (String suffix : fieldSuffix) {
      qParser =
          QParser.getParser(
              "foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 NOT_A_NUMBER)",
              req);
      qParser.setIsFilter(true); // this may change in the future
      SolrException e = expectThrows(SolrException.class, "Expecting exception", qParser::getQuery);
      assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
      assertTrue(
          "Unexpected exception: " + e.getMessage(),
          e.getMessage().contains("Invalid Number: NOT_A_NUMBER"));
    }
  }

  @Test
  public void testFieldExistsQueries() throws SyntaxError {
    SolrQueryRequest req = req();
    String[] fieldSuffix =
        new String[] {
          "ti",
          "tf",
          "td",
          "tl",
          "tdt", // trie types
          "pi",
          "pf",
          "pd",
          "pl",
          "pdt", // point types
          "i",
          "f",
          "d",
          "l",
          "dt",
          "s",
          "b", // numeric types
          "is",
          "fs",
          "ds",
          "ls",
          "dts",
          "ss",
          "bs", // multi-valued
          "i_dv",
          "f_dv",
          "d_dv",
          "l_dv",
          "dt_dv",
          "s_dv",
          "b_dv", // numerics + docValues
          "is_dv",
          "fs_dv",
          "ds_dv",
          "ls_dv",
          "dts_dv",
          "ss_dv",
          "bs_dv", // multi-docValues
          "i_dvo",
          "f_dvo",
          "d_dvo",
          "l_dvo",
          "dt_dvo", // not indexed
          "t",
          "t_on",
          "b_norms",
          "s_norms",
          "dt_norms",
          "i_norms",
          "l_norms",
          "f_norms",
          "d_norms"
        };
    String[] existenceQueries = new String[] {"*", "[* TO *]"};

    for (String existenceQuery : existenceQueries) {
      for (String suffix : fieldSuffix) {
        IndexSchema indexSchema = h.getCore().getLatestSchema();
        String field = "foo_" + suffix;
        String query = field + ":" + existenceQuery;
        QParser qParser = QParser.getParser(query, req);
        Query createdQuery = qParser.getQuery();
        SchemaField schemaField = indexSchema.getField(field);

        // Test float & double realNumber queries differently
        if ("[* TO *]".equals(existenceQuery)
            && (schemaField.getType().getNumberType() == NumberType.DOUBLE
                || schemaField.getType().getNumberType() == NumberType.FLOAT)) {
          assertFalse(
              "For float and double fields \""
                  + query
                  + "\" is not an existence query, so the query returned should not be a FieldExistsQuery.",
              createdQuery instanceof FieldExistsQuery);
          assertFalse(
              "For float and double fields \""
                  + query
                  + "\" is not an existence query, so the query returned should not be a FieldExistsQuery.",
              createdQuery instanceof FieldExistsQuery);
          assertFalse(
              "For float and double fields \""
                  + query
                  + "\" is not an existence query, so NaN should not be matched via a ConstantScoreQuery.",
              createdQuery instanceof ConstantScoreQuery);
          assertFalse(
              "For float and double fields\""
                  + query
                  + "\" is not an existence query, so NaN should not be matched via a BooleanQuery (NaN and [* TO *]).",
              createdQuery instanceof BooleanQuery);
        } else {
          if (schemaField.hasDocValues()) {
            assertTrue(
                "Field has docValues, so existence query \""
                    + query
                    + "\" should return FieldExistsQuery",
                createdQuery instanceof FieldExistsQuery);
          } else if (!schemaField.omitNorms()
              && !schemaField
                  .getType()
                  .isPointField()) { // TODO: Remove !isPointField() for SOLR-14199
            assertTrue(
                "Field has norms and no docValues, so existence query \""
                    + query
                    + "\" should return FieldExistsQuery",
                createdQuery instanceof FieldExistsQuery);
          } else if (schemaField.getType().getNumberType() == NumberType.DOUBLE
              || schemaField.getType().getNumberType() == NumberType.FLOAT) {
            assertTrue(
                "PointField with NaN values must include \"exists or NaN\" if the field doesn't have norms or docValues: \""
                    + query
                    + "\".",
                createdQuery instanceof ConstantScoreQuery);
            assertTrue(
                "PointField with NaN values must include \"exists or NaN\" if the field doesn't have norms or docValues: \""
                    + query
                    + "\".",
                ((ConstantScoreQuery) createdQuery).getQuery() instanceof BooleanQuery);
            assertEquals(
                "PointField with NaN values must include \"exists or NaN\" if the field doesn't have norms or docValues: \""
                    + query
                    + "\". This boolean query must be an OR.",
                1,
                ((BooleanQuery) ((ConstantScoreQuery) createdQuery).getQuery())
                    .getMinimumNumberShouldMatch());
            assertEquals(
                "PointField with NaN values must include \"exists or NaN\" if the field doesn't have norms or docValues: \""
                    + query
                    + "\". This boolean query must have 2 clauses.",
                2,
                ((BooleanQuery) ((ConstantScoreQuery) createdQuery).getQuery()).clauses().size());
          } else {
            assertFalse(
                "Field doesn't have docValues, so existence query \""
                    + query
                    + "\" should not return FieldExistsQuery",
                createdQuery instanceof FieldExistsQuery);
            assertFalse(
                "Field doesn't have norms, so existence query \""
                    + query
                    + "\" should not return FieldExistsQuery",
                createdQuery instanceof FieldExistsQuery);
          }
        }
      }
    }
  }
}
